S09-04 Webpack5-源码、自定义Loader、自定义Plugin
[TOC]
源码阅读
测试代码
1、在webpack源码中,编写测试文件夹why

2、直接在build.js中,调用webpack()函数,实现打包

3、使用node运行build.js

调试代码
1、添加断点

2、运行调试,点击Javascript调试终端 或 运行和调试

3、调试控制

编译入口文件


思维导图

插件
- Bookmarks
- CodeTour
自定义Loader
Loader API
自定义loader
xxx-loader():
(content, map, meta),自定义的loadercontent:``,资源文件的内容
map:``,sourcemap 相关的数据
meta:``,一些元数据
返回:
return:``,
this.callback():
(err, content),回调函数- err:
Error | null,错误信息。如果没错,则传入null - content:
string | buffer,传递给下个loader的内容
- err:
- js
module.exports = function (content, map, meta) { console.log('xxx-loader: ', content) // console.log('xxx-loader: ', map) // console.log('xxx-loader: ', meta) return 'xxx-loader' }
xxx-loader-pitch():
(remainingRequest, precedingRequest, data),一个函数,在加载过程中,Webpack 会调用它来处理文件。remainingRequest:
string,剩下的请求precedingRequest:
string,之前处理过的请求data:
object,loader 共享的数据返回:
不返回:``,继续执行后续的loader
返回值:``,终止执行后续的loader
- js
module.exports.pitch = function pitchLoader(remainingRequest, precedingRequest, data) { // `remainingRequest` 是从当前 loader 到最后一个 loader 的请求路径 // `precedingRequest` 是当前 loader 之前的所有 loader 的请求路径 + // `data` 是 loader 之间共享的数据 // 可以根据 remainingRequest 来决定是否继续处理 if (remainingRequest.includes('specificFile.js')) { // 如果特定文件存在,则忽略后续的 loader return `module.exports = 'This module is handled by pitch loader only.';`; } // 继续使用后续 loader // 返回 null 或者不返回任何值 };
loaderContext
每个 loader 都有一个 loaderContext 对象,loader函数内部的this指向该对象
this.async():
(),允许 loader 异步执行。调用async()方法可以使 loader 进入异步模式,并返回一个异步回调函数,用于处理异步操作。返回:
callback:
(err, content),返回一个异步回调函数,用于处理异步操作。err:
Error | null,错误信息。如果没错,则传入nullcontent:
string | buffer,传递给下个loader的内容
- js
module.exports = function(source) { const callback = this.async(); setTimeout(() => { callback(null, 'some processed code'); }, 1000); }
this.getOptions():
(),用于获取传递给 loader 的选项。返回:
options:
object,在 Webpack 配置中为 loader 指定的选项。- js
// webpack.config.js module.exports = { module: { rules: [ { test: /\.js$/, use: { loader: path.resolve(__dirname, 'my-loader.js'), options: { key1: 'value1', key2: 'value2' } } } ] } }; // loader函数 module.exports = function(source) { const options = this.getOptions(); console.log(options.key1); // 输出: 'value1' console.log(options.key2); // 输出: 'value2' // 处理源代码 return source; };
this.data:
,用来在不同 loader 之间共享数据:状态或配置信息。- js
// first-loader.js module.exports = function(source) { // 设置数据 this.data = { myData: 'Hello from first-loader' }; return source; }; // second-loader.js module.exports = function(source) { // 读取数据 const data = this.data.myData; console.log(data); // 输出: 'Hello from first-loader' // 继续处理源代码 return source; };
schema-utils
schema-utils:是一个用于验证和处理配置选项的库,通常与 Webpack 的 loader 和插件一起使用。它通过 JSON Schema 定义和验证选项,以确保它们符合预期的格式和类型。
validate():
(schema, options, context?),验证选项对象是否符合指定的 JSON Schema。schema:
object,定义选项格式的 JSON Schema 对象。options:
object,需要验证的选项对象。context?:
object,包含额外信息的上下文对象,例如name和baseDataPath。返回:
成功:不返回任何值。
失败:抛出错误。
- js
const { validate } = require('schema-utils'); const schema = { type: 'object', properties: { option1: { type: 'string' }, option2: { type: 'number' } }, required: ['option1'], additionalProperties: false }; const options = { option1: 'value', option2: 42 }; try { validate(schema, options, { name: 'my-loader' }); console.log('Options are valid!'); } catch (error) { console.error('Invalid options:', error); }
babel
babel: 是一个广泛使用的 JavaScript 编译器,它提供了一些核心 API 用于代码转换。
注意: 以下API属于 @babel/core 。每个API都有3种模式,如:
transform(): callbak模式,babel@8中将被删除。transformSync(): 同步模式transformAsync(): 异步Promise模式
API:
babel.transformSync():
(code, options),用于将源代码转换为不同版本的JS代码。code:
string,需要转换的 JS 源代码options:
{presets, plugins},用于配置 Babel 转换过程的选项。默认已经配置了presets返回值
result:
{code, map, ast},包含转换结果的对象,主要包括code(转换后的代码)和map(源映射)。- code:
string,转换后的JS代码。 - map:
object,生成的源映射(source map),如果启用了源映射的话。 - ast:
object,转换后的抽象语法树(AST),如果请求了ast选项的话。
- code:
- js
const babel = require('@babel/core'); const result = babel.transform('const a = 1;', { presets: ['@babel/preset-env'] }); console.log(result.code); // 编译后的代码
babel.parseSync():
(code, options),用于解析源代码并生成抽象语法树(AST)。code:
string,要解析的源代码。options:
{sourceType, plugins},解析的配置选项。返回:
ast:
object,生成的 AST 对象。- js
const parser = require('@babel/parser'); const ast = parser.parse('const a = 1;', { sourceType: 'module' }); console.log(ast);
transformFromAstSync():
(ast, code?, options),用于从 AST 进行转换为代码。ast:
object,要转换的抽象语法树。code?:
string,原始源代码(用于错误定位)。options:
object,Babel 编译选项。返回: Promise
result:
{code, map},返回一个对象,包含code(编译后的代码)、map(源映射)等信息。- js
const babel = require('@babel/core'); const parser = require('@babel/parser'); const generate = require('@babel/generator').default; const ast = parser.parse('const a = 1;'); const result = babel.transformFromAstSync(ast, 'const a = 1;', { presets: ['@babel/preset-env'] }); console.log(result.code); // 编译后的代码
loadPartialConfig():
(config?),用于加载或更新 Babel 的部分配置文件。config?:
object,包含了你想要更新或加载的部分配置选项。返回:
partialConfig:
PartialConfig,包含配置的对象,其中包括options和file。- js
const babel = require('@babel/core'); // 加载部分配置 const partialConfig = babel.loadPartialConfig({ presets: ['@babel/preset-env'], plugins: ['@babel/plugin-transform-arrow-functions'] }); // 访问配置和其他信息 console.log(partialConfig.config);
marked
marked: 是一个流行的 Markdown 解析器,将 Markdown 转换为 HTML。
API:
new Marked():
(...markedExtension[]),用于创建Marked实例的构造函数。markedExtension:
MarkedExtension,扩展的插件接口。主要用于在marked的解析过程中插入自定义逻辑。返回:
marked:
object,返回一个Marked实例。- js
// 使用自定义渲染器 const renderer = new marked.Renderer(); renderer.heading = (text, level) => `<h${level} class="custom-heading">${text}</h${level}>`; const markdown = '# Custom Heading'; const html = marked(markdown, { renderer }); console.log(html); // <h1 class="custom-heading">Custom Heading</h1>
marked.parse():
(markdown, options?),用于将 Markdown 文本转换为 HTML。markdown:
string,要解析的 Markdown 字符串。options:
object,配置选项对象,用于调整解析和渲染行为。- renderer:
boolean,自定义渲染器对象,用于修改 HTML 输出。 - gfm:
boolean,默认:true,是否启用 GitHub 风格的 Markdown 语法。 - breaks:
boolean,默认:false,是否将换行符转换为<br>标签。 - pedantic:
boolean,默认:false,是否宽容解析 Markdown 语法。 - sanitize:
boolean,默认:false,是否移除 HTML 标签。 - smartLists:
boolean,默认:false,是否优化列表输出。 - smartypants:
boolean,默认:false,是否使用智能引号。
- renderer:
返回:
html:
string,返回HTML字符串。- js
const marked = require('marked'); const markdown = '# Hello, World!\n\nThis is a paragraph with **bold** text.'; const html = marked.parse(markdown); console.log(html); // Output: // <h1>Hello, World!</h1> // <p>This is a paragraph with <strong>bold</strong> text.</p>
marked-highlight
marked-highlight:用于在Markdown中高亮代码的库。它将 marked 和 highlight.js结合。
API:
markedHighlight():
({highlight}),高亮代码块。highlight:
(code, lang) => void,转换代码为htmllangPrefix?:
string,默认:,class前缀async?:
boolean,默认:false,如果highlight方法返回一个Promise,就设置该选项为true返回:
markedExtension :
MarkedExtension,返回一个扩展的插件接口。主要用于在marked的解析过程中插入自定义逻辑。- js
const marked = new Marked( markedHighlight({ langPrefix: 'hljs language-', highlight: function (code, lang, info) { const language = hljs.getLanguage(lang) ? lang : 'plaintext' return hljs.highlight(code, { language }).value } }) ) const html = marked.parse(content)
highlight.js
highlight.js:用于高亮代码的库,支持多种编程语言。它的 API 允许你自定义代码高亮的行为。
API:
highlight():
(code, {language?}),用于高亮代码,支持指定语言。code:
string,要高亮的代码字符串。language?:
string,要使用的编程语言。如果未指定,highlight.js会尝试自动检测语言。返回:
result:
{value},返回一个对象,包含value属性,其中存储了高亮后的 HTML 字符串。- js
const hljs = require('highlight.js'); const code = 'const x = 42;'; const result = hljs.highlight(code, { language: 'javascript' }).value; console.log(result); // Output: <span class="hljs-keyword">const</span> x = <span class="hljs-number">42</span>;
getLanguage():
(lang),用于获取特定语言的定义。lang:
string,语言名称字符串,如'javascript'、'python'等。返回:
language:
object,返回一个包含语言定义的对象,或者如果语言未定义,则返回undefined。- js
const hljs = require('highlight.js'); // 用于检查 highlight.js 是否支持某种语言 const languageExists = hljs.getLanguage('javascript') !== undefined; console.log(languageExists); // Output: true
自定义Loader
Loader 是用于对模块的源代码进行转换(处理),之前我们已经使用过很多 Loader,比如 css-loader、style-loader、babel-loader 等。
这里我们来学习如何自定义自己的 Loader:
- Loader 本质上是一个导出为函数的 JavaScript 模块;
- 注意: loader导出时,建议使用commonjs语法导出:
module.exports或exports - loader-runner 库会调用这个函数,然后将上一个 loader 产生的结果或者资源文件传入进去;
- 注意: loader最终返回的结果必须是模块化的内容
自定义loader
编写一个 xxx-loader01.js 模块这个函数会接收三个参数:
xxx-loader():
(content, map, meta),自定义的loadercontent:``,资源文件的内容
map:``,sourcemap 相关的数据
meta:``,一些元数据
返回:
return:``,
this.callback():
(err, content),回调函数- err:
Error | null,错误信息。如果没错,则传入null - content:
string | buffer,传递给下个loader的内容
- err:
- js
module.exports = function (content, map, meta) { console.log('xxx-loader: ', content) // console.log('xxx-loader: ', map) // console.log('xxx-loader: ', meta) return 'xxx-loader' }
1、自定义一个loader

2、使用loader

3、使用多个loader

4、打包效果

resolveLoader
- resolveLoader:这组选项与 resolve 对象的属性集合相同, 但仅用于解析 webpack 的 loader 包。
- modules:
string[],告诉 webpack 解析模块时应该搜索的目录,默认值:['node_modules']
- modules:
如果我们依然希望可以直接去加载自己的 loader 文件夹,有没有更加简洁的办法呢?
1、配置 resolveLoader 属性
注意: 传入的路径和 context 是有关系的,在前面我们讲入口的相对路径时有讲过。context会影响到entry和loader中的路径起始点

2、此时loader就可以这样写了,也可以找到

loader执行顺序
loader执行顺序
创建多个 Loader 使用,它的执行顺序是什么呢?
- 从后向前、从右向左


pitch-loader
事实上还有另一种 Loader,称之为 PitchLoader:
pitch loader: 允许你在真正的 loader 之前插入逻辑,并可以决定是否继续处理后续的 loader。
语法:
pitch loader 是一个函数,在加载过程中,Webpack 会调用它来处理文件。
xxx-loader-pitch():
(remainingRequest, precedingRequest, data),一个函数,在加载过程中,Webpack 会调用它来处理文件。remainingRequest:
string,剩下的请求precedingRequest:
string,之前处理过的请求data:
object,loader 共享的数据
返回:
不返回:``,继续执行后续的loader
返回值:``,终止执行后续的loader
- js
module.exports.pitch = function pitchLoader(remainingRequest, precedingRequest, data) { // `remainingRequest` 是从当前 loader 到最后一个 loader 的请求路径 // `precedingRequest` 是当前 loader 之前的所有 loader 的请求路径 + // `data` 是 loader 之间共享的数据 // 可以根据 remainingRequest 来决定是否继续处理 if (remainingRequest.includes('specificFile.js')) { // 如果特定文件存在,则忽略后续的 loader return `module.exports = 'This module is handled by pitch loader only.';`; } // 继续使用后续 loader // 返回 null 或者不返回任何值 };


enforce控制执行顺序
loader执行顺序的内部实现:
其实这也是为什么 loader 的执行顺序是相反的:
run-loader 优先执行 PitchLoader,在执行 PitchLoader 时进行 loaderIndex++;
run-loader 之后执行 NormalLoader,在执行 NormalLoader 时进行 loaderIndex--;
修改loader执行顺序:
- rules:
[{test, use, loader, type, exclude, include, parser, generator, enforce},...],规则集合test:
reg,匹配文件资源use:
[{loader, options, query},...],设置对匹配到的资源使用的loader及配置- loader:
string,使用的loader。示例:use: [{loader: 'css-loader'}],简写:use: ['css-loader'] - options:
object,loader的配置项。示例:use: [{loader: 'css-loader', options: {importLoaders: 1}}], - query:``,已被options替代
- 注意: use中多个loader的使用顺序是从后往前的
- loader:
loader:
,Rule.use[{loader}]的简写enforce:
pre | post | normal | inline,用于控制 loader 执行顺序的选项。pre:指定该 loader 在所有其他 loader 之前执行。常用于进行某些预处理,例如代码风格检查。post:指定该 loader 在所有其他 loader 之后执行。常用于进行某些后处理,例如添加某些特性或优化。normal:默认,loader 将按照 Webpack 的默认顺序执行。inline:在行内设置的 loader。如:import 'loader1!loader2!./test.js'- js
rules: [ { test: /\.js$/, use: [{ loader: 'xxx-loader' }], enforce: 'pre' // 1 }, { // 2 test: /\.js$/, use: [{ loader: 'yyy-loader' }] }, { test: /\.js$/, use: [{ loader: 'zzz-loader' }], enforce: 'post' // 3 } ]
那么,能不能改变它们的执行顺序呢?
- 我们可以拆分成多个 Rule 对象,通过 enforce 来改变它们的顺序;
在 Pitching 和 Normal 它们的执行顺序分别是:
Pitching: post, inline, normal, pre;
Normal: pre, normal, inline, post;


同步、异步Loader
同步Loader
什么是同步的 Loader 呢?
默认创建的 Loader 就是同步的 Loader;
这个 Loader 必须通过 return 或者 this.callback 来返回结果,交给下一个 loader 来处理;
通常在有错误的情况下,我们会使用 this.callback;
this.callback 的用法如下:
- this.callback():
(err, content),回调函数- err:
Error | null,错误信息。如果没错,则传入null - content:
string | buffer,传递给下个loader的内容
- err:

异步Loader
什么是异步的 Loader 呢?
有时候我们使用 Loader 时会进行一些异步的操作;
我们希望在异步操作完成后,再返回这个 loader 处理的结果;
这个时候我们就要使用异步的 Loader 了;
loader-runner 已经在执行 loader 时给我们提供了方法,让 loader 变成一个异步的 loader:
// 通过调用this.async(),告诉loader不要在函数末尾直接return undefined,而是返回异步操作返回的结果
const callback = this.async()
参数
传入、获取参数
1、传递参数

2、获取参数
- 方式一:(废弃),早期需要使用 loader-utils 库来获取参数,目前已经不再需要
- 方式二:目前可以直接通过
this.getOptions()方法获取参数


校验参数
1、我们可以通过一个 webpack 官方提供的校验库 schema-utils 安装对应的库:
npm install schema-utils -D2、传递参数

3、校验规则

4、校验参数是否符合规则

5、校验失败

案例
mr-babel-loader
我们知道 babel-loader 可以帮助我们对 JavaScript 的代码进行转换,这里我们定义一个自己的 babel-loader:
一、依赖包: @babel/core
二、实现过程
1、使用 babel.transform() 方法转换js代码

此时打包会发现babel并没有转换ES6的语法为ES5
2、在使用babel-loader时,传递plugins、presets打包参数


3、在自定义的babel-loader中获取传递的options参数



4、添加参数校验


5、在babel.config.js文件中配置babel参数
配置参数

获取参数


mr-md-loader
作用: hymd-loader用来解析markdown文件
依赖包:
- marked:marked 是一个基于JS的 Markdown 解析器和编译器。
- 安装:
pnpm i marked -D
- 安装:
- highlight.js:代码高亮插件。
- 安装:
pnpm i highlight.js -D
- 安装:
- marked-highlight:用于在Markdown中高亮代码的库。它将 marked 和 highlight.js结合。
- 安装:
pnpm i marked-highlight -D
- 安装:
实现过程:
1、使用mr-md-loader解析md文件

2、mr-md-loader基本实现
由于loader返回的结果必须是一个模块化的内容,此处在得到html文本后需要保存到code变量并导出出去。

3、在main.js中导入md文件,并显示到页面中

4、显示效果
问题:此时的样式优点丑,需要优化样式

5、优化: 添加自定义的CSS样式
css样式

导入样式

配置css-loader

效果

6、优化: 高亮关键字
使用
highlight.js插件标识出md内容的关键字

自定义关键字的样式



使用
highlight.js库默认的样式

自定义Plugin
Plugin API
tapable
tapable: 是一个用于处理插件系统的 JS 库,通常用于构建和扩展系统中的钩子(hooks)和事件。这是一个被广泛使用的库,尤其是在 Webpack 和其他构建工具中。
API:
实例方法
hook.tap():
(pluginName, fn),用于同步钩子的注册方法。它用于注册一个钩子函数,并指定一个插件名称。pluginName:
string,插件名称。fn:
(...args) => void,钩子函数。- js
hook.tap('SecondPlugin', (name, age) => { console.log('SecondPlugin:', name, age); });
hook.tapAsync():
(pluginName, fn),用于注册异步钩子的方法,适用于AsyncSeriesHook和AsyncParallelHook等异步钩子类型。pluginName:
string,插件名称。fn:
(...args, callback) => void,钩子函数。在操作完成后调用 callback()。- js
hookAsync.tapAsync('SecondPlugin', (name, callback) => { setTimeout(() => { console.log('SecondPlugin:', name); callback(); // 完成异步操作 }, 500); });
hook.call():
(...args),用于同步地触发所有注册的钩子函数。按注册顺序执行。...args:
string[],指定钩子所需的参数。- js
const { SyncHook } = require('tapable'); // 创建一个 SyncHook 实例 const hook = new SyncHook(['name', 'age']); // 注册钩子函数 hook.tap('PrintName', (name, age) => { console.log(`Name: ${name},Age: ${age}`); }); // 触发钩子 hook.call('Alice', 30);
hook.callAsync():
(...args, callback),用于触发异步钩子的一个方法,并在所有钩子完成后执行一个最终的回调。...args:
string[],传递给钩子函数的参数。callback:
(err?) => void,所有钩子函数执行完成后的回调函数,接受一个可选的错误参数。- js
const { AsyncSeriesHook } = require('tapable'); // 创建 AsyncSeriesHook 实例 const hook = new AsyncSeriesHook(['arg1', 'arg2']); // 注册异步钩子 hook.tapAsync('Plugin1', (arg1, arg2, callback) => { setTimeout(() => { console.log('Plugin1:', arg1, arg2); callback(); // 异步操作完成后调用 callback }, 1000); }); hook.tapAsync('Plugin2', (arg1, arg2, callback) => { setTimeout(() => { console.log('Plugin2:', arg1, arg2); callback(); // 异步操作完成后调用 callback }, 500); }); // 触发钩子 hook.callAsync('value1', 'value2', (err) => { if (err) { console.error('Error:', err); } else { console.log('All hooks executed'); } });
创建实例
new SyncHook():
([arg1, arg2, ...]),用于创建同步钩子的类。[arg1, arg2, ...]:
string[],定义了钩子函数所接受的参数列表。返回:
hook:
SyncHook,一个 SyncHook 实例。- js
const { SyncHook } = require('tapable'); // 创建一个 SyncHook 实例 const hook = new SyncHook(['name', 'age']); // 注册钩子函数 hook.tap('PrintName', (name, age) => { console.log(`Name: ${name},Age: ${age}`); }); // 触发钩子 hook.call('Alice', 30);
new SyncBailHook():
([arg1, arg2, ...]),用于创建同步钩子的类,与SyncHook不同的是,它提供了一个“中断”机制。一旦一个钩子函数返回非undefined的值,后续的钩子函数将不会被执行。[arg1, arg2, ...]:
string[],定义了钩子函数所接受的参数列表。返回:
hook:
SyncBailHook,一个 SyncBailHook 实例。- js
const { SyncBailHook } = require('tapable'); // 创建一个 SyncBailHook 实例 const hook = new SyncBailHook(['data']); // 注册钩子函数 hook.tap('FirstPlugin', (data) => { console.log('FirstPlugin:', data); // 返回一个值,后续钩子将不会被调用 return 'Early exit'; }); hook.tap('SecondPlugin', (data) => { console.log('SecondPlugin:', data); }); // 触发钩子 hook.call('Hello, World!');
new SyncLoopHook():
([arg1, arg2, ...]),用于创建同步钩子的类,允许注册的钩子函数在特定条件下重复执行。每个钩子函数在被调用时会持续执行直到它返回undefined,然后停止循环。[arg1, arg2, ...]:
string[],定义了钩子函数所接受的参数列表。返回:
hook:
SyncLoopHook,一个 SyncLoopHook 实例。- js
const { SyncLoopHook } = require('tapable'); // 创建一个 SyncLoopHook 实例 const hook = new SyncLoopHook(['count']); // 注册钩子函数 hook.tap('LoopPlugin', (count) => { console.log('Processing:', count); // 返回非 undefined 的值,继续循环 if (count > 0) { return count - 1; } // 返回 undefined,停止循环 return undefined; }); // 触发钩子 hook.call(5);
new SyncWaterfallHook():
([arg1, arg2, ...]),用于创建同步钩子的类,它的特点是钩子函数的返回值会传递给下一个钩子函数。[arg1, arg2, ...]:
string[],定义了钩子函数所接受的参数列表。返回:
hook:
SyncWaterfallHook,一个 SyncWaterfallHook 实例。- js
const { SyncWaterfallHook } = require('tapable'); // 创建一个 SyncWaterfallHook 实例 const hook = new SyncWaterfallHook(['data']); // 注册钩子函数 hook.tap('FirstPlugin', (data) => { console.log('FirstPlugin:', data); // 修改数据并传递给下一个钩子函数 return data + 1; }); hook.tap('SecondPlugin', (data) => { console.log('SecondPlugin:', data); // 修改数据并传递给下一个钩子函数 return data * 2; }); hook.tap('ThirdPlugin', (data) => { console.log('ThirdPlugin:', data); // 最后一个钩子函数的返回值将不会被传递给其他函数 return data - 3; }); // 触发钩子 hook.call(5);
new AsyncParallelHook():
([arg1, arg2, ...]),用于创建异步并行执行的钩子。与同步钩子不同,异步并行钩子允许钩子函数并行执行而不是依次执行。[arg1, arg2, ...]:
string[],定义了钩子函数所接受的参数列表。返回:
hookAsync:
AsyncParallelHook,一个 AsyncParallelHook 实例。- js
const { AsyncParallelHook } = require('tapable'); // 创建一个 AsyncParallelHook 实例 const hookAsync = new AsyncParallelHook(['name']); // 注册钩子函数 hookAsync.tapAsync('FirstPlugin', (name, callback) => { setTimeout(() => { console.log('FirstPlugin:', name); callback(); // 完成异步操作 }, 1000); }); hookAsync.tapAsync('SecondPlugin', (name, callback) => { setTimeout(() => { console.log('SecondPlugin:', name); callback(); // 完成异步操作 }, 500); }); // 触发钩子 hookAsync.callAsync('John', (err) => { if (err) { console.error('Error:', err); } else { console.log('All plugins have finished processing'); } });
new AsyncSeriesHook():
([arg1, arg2, ...]),用于处理异步操作。会等待上一个异步的 Hook 执行完毕。[arg1, arg2, ...]:
string[],定义了钩子函数所接受的参数列表。返回:
hookAsync:
AsyncSeriesHook,一个 AsyncSeriesHook 实例。- js
const { AsyncSeriesHook } = require('tapable'); // 创建 AsyncSeriesHook 实例 const hook = new AsyncSeriesHook(['arg1', 'arg2']); // 注册钩子 hook.tapAsync('MyPlugin1', (arg1, arg2, callback) => { // 异步操作 setTimeout(() => { console.log('Async operation complete'); callback(); // 需要调用 callback 来表示操作完成 }, 1000); }); // 会等MyPlugin1执行完毕才执行 hook.tapAsync('MyPlugin2', (arg1, arg2, callback) => { // 异步操作 setTimeout(() => { console.log('Async operation complete'); callback(); // 需要调用 callback 来表示操作完成 }, 1000); }); // 触发钩子 hook.callAsync('value1', 'value2', (err) => { if (err) { console.error('Error:', err); } else { console.log('All hooks executed'); } });
node-ssh
node-ssh:是一个用于在 Node.js 中简化 SSH 连接和命令执行的库。它提供了一种方便的方式来进行远程管理和自动化操作。
API:
new NodeSSH():
(),创建SSH实例。返回
ssh:
NodeSSH,SSH实例- js
const { NodeSSH } = require('node-ssh'); const ssh = new NodeSSH();
ssh.connect():
({host, port, username, password, privateKey}),连接到远程服务器。host:
string,远程服务器的主机名或 IP 地址。port:
number,默认:22,SSH 端口。username:
string,登录用户名。password:
string,登录密码(如果你使用密码认证)。privateKey:
string,私钥文件路径(如果你使用密钥认证)。返回: Promise
promise:
() => void,- js
await ssh.connect({ host: 'example.com', username: 'your-username', password: 'your-password' });
ssh.execCommand():
(command,options?),在远程主机上执行linux命令。command:
string,要执行的linux命令。options?:
{cwd, stdin},命令执行的选项,包括cwd(当前工作目录) 和stdin(标准输入数据)。返回: Promise
result:
({stdout, stderr}) => void,返回一个包含stdout和stderr的Promise对象。- js
const result = await ssh.execCommand('ls -la'); console.log('STDOUT:', result.stdout); console.log('STDERR:', result.stderr);
ssh.putFile():
(localPath, remotePath),将本地文件上传到远程主机。localPath:
string,本地文件的路径。remotePath:
string,远程主机上的目标路径。返回: Promise
- js
await ssh.putFile('local/file/path', 'remote/file/path');
ssh.getFile():
(localPath, remotePath),从远程主机下载文件到本地。localPath:
string,本地目标路径。remotePath:
string,远程文件的路径。返回: Promise
- js
await ssh.getFile('local/file/path', 'remote/file/path');
ssh.putDirectory():
(localDir, remoteDir,options?),将本地目录递归地上传到远程服务器。localDir:
string,本地目录的路径,指定要上传的目录。remoteDir:
string,远程服务器上的目标目录路径。options?:
{recursive?, concurrency?, overwrite?, tick?},配置上传的选项。- recursive?:
boolean,默认:true,是否递归地上传子目录 - concurrency?:
number,默认:10,并发上传的并发数。 - overwrite?:
boolean,默认:true,是否覆盖远程目录中已存在的文件。 - tick?:
(localPath, remotePath, error) => void,回调函数,接收每个文件的上传进度。
- recursive?:
返回: Promise
- js
const localDir = path.resolve(__dirname, 'local-directory'); const remoteDir = '/path/on/remote/server'; await ssh.putDirectory(localDir, remoteDir, { recursive: true, overwrite: true, tick: (localPath, remotePath, error) => { if (error) { console.error(`Failed to upload ${localPath}:`, error); } else { console.log(`Uploaded ${localPath} to ${remotePath}`); } } });
ssh.getDirectory():
(localDir, remoteDir,options?),从远程服务器递归地下载目录到本地。localDir:
string,本地目标目录的路径。remoteDir:
string,远程服务器上的源目录路径。options?:
{recursive?, concurrency?, overwrite?, tick?},配置下载的选项。- recursive?:
boolean,默认:true,是否递归地下载子目录 - concurrency?:
number,默认:10,并发下载的并发数。 - overwrite?:
boolean,默认:true,是否覆盖远程目录中已存在的文件。 - tick?:
(localPath, remotePath, error) => void,回调函数,接收每个文件的下载进度。
- recursive?:
- js
const remoteDir = '/path/on/remote/server'; const localDir = path.resolve(__dirname, 'local-directory'); await ssh.getDirectory(remoteDir, localDir, { recursive: true, overwrite: true, tick: (remotePath, localPath, error) => { if (error) { console.error(`Failed to download ${remotePath}:`, error); } else { console.log(`Downloaded ${remotePath} to ${localPath}`); } } });
ssh.dispose():
(),关闭与远程主机的连接并清理资源。- js
ssh.dispose();
Tapable
Tapable概述
我们知道 webpack 有两个非常重要的类:Compiler 和 Compilation
他们通过注入插件的方式,来监听 webpack 的所有生命周期;
插件的注入离不开各种各样的 Hook,而他们的 Hook 是如何得到的呢?
其实是创建了 Tapable 库中的各种 Hook 的实例;
所以,如果我们想要学习自定义插件,最好先了解一个库:Tapable
Tapable 是官方编写和维护的一个库;
Tapable 管理着需要的Hook,这些Hook可以被应用到我们的插件中;
Tapable的Hook

同步和异步的:
以 sync 开头的,是同步的Hook。
以 async 开头的,是异步的Hook,两个事件处理回调,不会等待上一次处理回调结束后再执行下一次回调。
其他的类别:
Bail:当有返回值时,就不会执行后续的事件触发了;
Loop:当返回值为 true,就会反复执行该事件,当返回值为 undefined 或者不返回内容,就退出事件,执行下一个事件;
Waterfall:当返回值不为 undefined 时,会将这次返回的结果作为下次事件的第一个参数;
Parallel:并行,不会等到上一个事件回调执行结束,才执行下一次事件处理回调;
Series:串行,会等待上一个异步的 Hook;
Hook的使用
依赖包:
- tapable:通过提供 Hooks 系统,使得你可以在 Webpack 的构建流程中插入自定义的逻辑。
- 安装:
pnpm i tapable
- 安装:
sync-基本使用
1、创建Hook对象

2、监听Hook中的事件
注意: 自定义的插件就是写在这个位置

3、触发事件

sync-bail使用
Bail:当有返回值时,就不会执行后续的事件触发了。
1、创建bailHook

2、监听Hook中的事件


3、触发事件

sync-loop使用
Loop:当返回值为 true,就会反复执行该事件,当返回值为 undefined 或者不返回内容,就退出事件,执行下一个事件;
1、创建loopHook

2、监听Hook中的事件


3、触发事件

sync-waterfall使用
Waterfall:当返回值不为 undefined 时,会将这次返回的结果作为下次事件的第一个参数;
1、创建waterfallHook

2、监听Hook中的事件


3、触发事件

async-parallel使用
Parallel:并行,不会等到上一个事件回调执行结束,才执行下一次事件处理回调;
1、创建parallelHook

2、监听Hook中的事件


3、触发事件

async-series使用
Series:串行,会等待上一个异步的 Hook;
1、创建seriesHook

2、监听Hook中的事件


3、触发事件

自定义Plugin
在之前的学习中,我们已经使用了非常多的 Plugin:
CleanWebpackPlugin
HTMLWebpackPlugin
MiniCSSExtractPlugin
CompressionPlugin
等等。。。
这些 Plugin 是如何被注册到 webpack 的生命周期中的呢?
第一:在 webpack 函数的 createCompiler 方法中,注册了所有的插件;
第二:在注册插件时,会调用插件函数或者插件对象的 apply 方法;
第三:插件方法会接收 compiler 对象,我们可以通过 compiler 对象来监听 Hook 的事件;
第四:某些插件也会传入一个 compilation 的对象,我们也可以监听 compilation 的 Hook 事件;
auto-upload-webpack-plugin
如何开发自己的插件呢?
目前大部分插件都可以在社区中找到,但是推荐尽量使用在维护,并且经过社区验证的;
这里我们开发一个自己的插件:将静态文件自动上传服务器中;
依赖包:
- node-ssh:在node中通过ssh连接远程服务器
- 安装:
pnpm i node-ssh -D
- 安装:
自定义插件:
1、创建 AutoUploadWebpackPlugin 类;

2、编写 apply 方法:
获取输出文件夹路径
通过 ssh 连接服务器;
删除服务器原来的文件夹;
上传文件夹中的内容;
const { NodeSSH } = require('node-ssh')
const { PASSWORD } = require('./config')
class AutoUploadWebpackPlugin {
constructor(options) {
this.ssh = new NodeSSH()
this.options = options
}
apply(compiler) {
// console.log("AutoUploadWebpackPlugin被注册:")
// 完成的事情: 注册hooks监听事件
// 等到assets已经输出到output目录上时, 完成自动上传的功能
compiler.hooks.afterEmit.tapAsync("AutoPlugin", async (compilation, callback) => {
// 1.获取输出文件夹路径(其中资源)
const outputPath = compilation.outputOptions.path
// 2.连接远程服务器 SSH
await this.connectServer()
// 3.删除原有的文件夹中内容
const remotePath = this.options.remotePath
this.ssh.execCommand(`rm -rf ${remotePath}/*`)
// 4.将文件夹中资源上传到服务器中
await this.uploadFiles(outputPath, remotePath)
// 5.关闭ssh连接
this.ssh.dispose()
// 完成所有的操作后, 调用callback()
callback()
})
}
async connectServer() {
await this.ssh.connect({
host: this.options.host,
username: this.options.username,
password: this.options.password
})
console.log('服务器连接成功')
}
async uploadFiles(localPath, remotePath) {
const status = await this.ssh.putDirectory(localPath, remotePath, {
recursive: true,
concurrency: 10
})
if (status) {
console.log("文件上传服务器成功~")
}
}
}
module.exports = AutoUploadWebpackPlugin
module.exports.AutoUploadWebpackPlugin = AutoUploadWebpackPlugin3、在 webpack 的 plugins 中,使用 AutoUploadWebpackPlugin 类;
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const AutoUploadWebpackPlugin = require('./plugins/AutoUploadWebpackPlugin')
const { PASSWORD } = require('./plugins/config')
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "./build"),
filename: "bundle.js"
},
plugins: [
new HtmlWebpackPlugin(),
new AutoUploadWebpackPlugin({
host: "123.207.32.32",
username: "root",
password: PASSWORD,
remotePath: "/root/test"
})
]
}